using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;

namespace ScytheEngine
{
	/// <summary>
	/// The screen manager is a component which manages one or more GameState
	/// instances. It maintains a stack of screens, calls their Update and Draw
	/// methods at the appropriate times, and automatically routes input to the
	/// topmost active screen.
	/// </summary>
	public class GameStateManager : DrawableGameComponent
	{
		#region Fields

		List<GameState> screens = new List<GameState>();
		List<GameState> screensToUpdate = new List<GameState>();

		InputState input = new InputState();

		// special screens
		Type m_MainMenuScreenType;
		Type m_GamePausedScreenType;

		IGraphicsDeviceService graphicsDeviceService;

		SceneManager sceneManager;
		ContentManager content;
		SpriteBatch spriteBatch;
		SpriteFont font;
		Texture2D blankTexture;

		bool traceEnabled;
		bool isInitialized;

		#endregion

		#region Properties


		/// <summary>
		/// Expose access to our Game instance (this is protected in the
		/// default GameComponent, but we want to make it public).
		/// </summary>
		new public Game Game
		{
			get { return base.Game; }
		}


		/// <summary>
		/// A content manager used to load data that is shared between multiple
		/// screens. This is never unloaded, so if a screen requires a large amount
		/// of temporary data, it should create a local content manager instead.
		/// </summary>
		public ContentManager Content
		{
			get { return content; }
		}

		/// <summary>
		/// The global scene manager where all scene elements of the game live
		/// </summary>
		public SceneManager SceneManager
		{
			get { return sceneManager; }
		}

		/// <summary>
		/// A default SpriteBatch shared by all the screens. This saves
		/// each screen having to bother creating their own local instance.
		/// </summary>
		public SpriteBatch SpriteBatch
		{
			get { return spriteBatch; }
		}


		/// <summary>
		/// A default font shared by all the screens. This saves
		/// each screen having to bother loading their own local copy.
		/// </summary>
		public SpriteFont Font
		{
			get { return font; }
		}


		/// <summary>
		/// If true, the manager prints out a list of all the screens
		/// each time it is updated. This can be useful for making sure
		/// everything is being added and removed at the right times.
		/// </summary>
		public bool TraceEnabled
		{
			get { return traceEnabled; }
			set { traceEnabled = value; }
		}

		#endregion

		#region Initialization


		/// <summary>
		/// Constructs a new screen manager component.
		/// </summary>
		public GameStateManager( ScytheEngineGame game )
			: base( game )
		{
			content = new ContentManager( game.Services );
			sceneManager = game.SceneManager;

			graphicsDeviceService = (IGraphicsDeviceService)game.Services.GetService(
														typeof( IGraphicsDeviceService ) );

			if ( graphicsDeviceService == null )
				throw new InvalidOperationException( "No graphics device service." );
		}

		/// <summary>
		/// Initializes the screen manager component.
		/// </summary>
		public override void Initialize()
		{
			base.Initialize();

			isInitialized = true;
		}


		/// <summary>
		/// Load your graphics content.
		/// </summary>
		protected override void LoadContent()
		{
			// Load content belonging to the screen manager.
			ContentManager content = Game.Content;

			spriteBatch = new SpriteBatch( GraphicsDevice );
			font = content.Load<SpriteFont>( "menufont" );
			blankTexture = content.Load<Texture2D>( "blank" );

			// Tell each of the screens to load their content.
			foreach ( GameState screen in screens )
			{
				screen.LoadContent();
			}
		}


		/// <summary>
		/// Unload your graphics content.
		/// </summary>
		protected override void UnloadContent()
		{
			// Tell each of the screens to unload their content.
			foreach ( GameState screen in screens )
			{
				screen.UnloadContent();
			}
		}

		#endregion

		#region Update and Draw


		/// <summary>
		/// Allows each screen to run logic.
		/// </summary>
		public override void Update( GameTime gameTime )
		{
			// Read the keyboard and gamepad.
			input.Update();

			// Make a copy of the master screen list, to avoid confusion if
			// the process of updating one screen adds or removes others.
			screensToUpdate.Clear();

			foreach ( GameState screen in screens )
				screensToUpdate.Add( screen );

			bool otherScreenHasFocus = !Game.IsActive;
			bool coveredByOtherScreen = false;

			// Loop as long as there are screens waiting to be updated.
			while ( screensToUpdate.Count > 0 )
			{
				// Pop the topmost screen off the waiting list.
				GameState screen = screensToUpdate[screensToUpdate.Count - 1];

				screensToUpdate.RemoveAt( screensToUpdate.Count - 1 );

				// Update the screen.
				screen.Update( gameTime, otherScreenHasFocus, coveredByOtherScreen );

				if ( screen.ScreenState == ScreenState.TransitionOn ||
					screen.ScreenState == ScreenState.Active )
				{
					// If this is the first active screen we came across,
					// give it a chance to handle input.
					if ( !otherScreenHasFocus )
					{
						screen.HandleInput( input );

						otherScreenHasFocus = true;
					}

					// If this is an active non-popup, inform any subsequent
					// screens that they are covered by it.
					if ( !screen.IsPopup )
						coveredByOtherScreen = true;
				}
			}

			// Print debug trace?
			if ( traceEnabled )
				TraceScreens();
		}


		/// <summary>
		/// Prints a list of all the screens, for debugging.
		/// </summary>
		void TraceScreens()
		{
			List<string> screenNames = new List<string>();

			foreach ( GameState screen in screens )
				screenNames.Add( screen.GetType().Name );

			Trace.WriteLine( string.Join( ", ", screenNames.ToArray() ) );
		}

		/// <summary>
		/// Tells each screen to draw itself.
		/// </summary>
		public override void Draw( GameTime gameTime )
		{
			foreach ( GameState screen in screens )
			{
				if ( screen.ScreenState == ScreenState.Hidden )
					continue;

				screen.Draw( gameTime );
			}
		}

		#endregion

		#region Public Methods


		/// <summary>
		/// Adds a new screen to the screen manager.
		/// </summary>
		public void AddState( GameState screen )
		{
			screen.GameStateManager = this;
			screen.IsExiting = false;
			
			// If we have a graphics device, tell the screen to load content.
			if ( isInitialized )
			{
				screen.LoadContent();
			} 
			
			screens.Add( screen );
		}

		public void SetSpecialScreenType( Type type, ScreenType screen_type)
		{
			switch(screen_type)
			{
				case ScreenType.MainMenuScreen:
					m_MainMenuScreenType = type;
					break;
				case ScreenType.GamePausedScreen:
					m_GamePausedScreenType = type;
					break;
			}
		}

		public MenuScreen GetSpecialScreen(ScreenType screen_type)
		{
			ConstructorInfo constructor_info = null;
			switch ( screen_type )
			{
				case ScreenType.MainMenuScreen:
					constructor_info = m_MainMenuScreenType.GetConstructor( new Type[] { } );
					break;
				case ScreenType.GamePausedScreen:
					constructor_info = m_GamePausedScreenType.GetConstructor( new Type[] { } );
					break;
			}
			return (MenuScreen)constructor_info.Invoke(constructor_info.GetParameters());
		}


		/// <summary>
		/// Removes a screen from the screen manager. You should normally
		/// use GameState.ExitScreen instead of calling this directly, so
		/// the screen can gradually transition off rather than just being
		/// instantly removed.
		/// </summary>
		public void RemoveState( GameState screen )
		{
			// If we have a graphics device, tell the screen to unload content.
			if ( isInitialized )
			{
				screen.UnloadContent();
			} 
			
			screens.Remove( screen );
			screensToUpdate.Remove( screen );
		}


		/// <summary>
		/// Expose an array holding all the screens. We return a copy rather
		/// than the real master list, because screens should only ever be added
		/// or removed using the AddScreen and RemoveScreen methods.
		/// </summary>
		public GameState[] GetScreens()
		{
			return screens.ToArray();
		}


		/// <summary>
		/// Helper draws a translucent black fullscreen sprite, used for fading
		/// screens in and out, and for darkening the background behind popups.
		/// </summary>
		public void FadeBackBufferToBlack( int alpha )
		{
			Viewport viewport = graphicsDeviceService.GraphicsDevice.Viewport;

			spriteBatch.Begin();

			spriteBatch.Draw( blankTexture,
							 new Rectangle( 0, 0, viewport.Width, viewport.Height ),
							 new Color( 0, 0, 0, (byte)alpha ) );

			spriteBatch.End();
		}


		#endregion
	}
}
